﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Profile;
using RAD.LightOne;
using System.Configuration;
using RAD.LightOne.Data;
using System.Data;
using System.Configuration.Provider;

namespace RAD.LightOne.Web.Security
{
    #region Script
    /*
    SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[Profiles](
    [Id] [char](36) NOT NULL,
    [Class] [varchar](256) NULL,
    [Register] [datetime] NULL,
    [Username] [varchar](255) NOT NULL,
    [ApplicationName] [varchar](255) NOT NULL,
    [IsAnonymous] [bit] NULL,
    [LastActivityDate] [datetime] NULL,
    [LastUpdatedDate] [datetime] NULL,
 CONSTRAINT [PK_Profiles_ID] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
 CONSTRAINT [IX_UN_Username_AppName] UNIQUE NONCLUSTERED 
(
    [Username] ASC,
    [ApplicationName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO
     */
    #endregion
    #region Documentation
    /// <summary>
    /// Provedor de perfil(profile) integrado ao LightOne.
    /// </summary>
    /// <seealso cref="http://msdn.microsoft.com/en-us/library/0580x1f5.ASPX"/>
    #endregion
    public sealed class LightOneProfileProvider : ProfileProvider
    {
        #region InnerTypes

        #region Documentation
        /// <summary>
        /// Interface que define o armazenamento configurável para consultas do provider, que pode variar de banco a banco.
        /// </summary>
        #endregion
        public interface IQueryStorage
        {
            string GetProfileInfo { get; }
            string GetProfileInfoTotalRecords { get; }
        }

        #region Documentation
        /// <summary>
        /// Provedor de armazenamento de expressões para utilização interna.
        /// </summary>
        #endregion
        private static class QueryStorageProvider
        {
            private static IQueryStorage instance;
            private static object locker;

            static QueryStorageProvider()
            {
                locker = new object();
            }

            public static IQueryStorage Instance(string type)
            {
                lock (locker)
                {
                    Type t = Type.GetType(type);
                    if (instance == null || instance.GetType() != t)
                    {
                        instance = (IQueryStorage)Activator.CreateInstance(t);
                    }
                    return instance;
                }
            }
        }

        #region Documentation
        /// <summary>
        /// Implementação concreta do armazenamento, aderente ao Sql Server 2012.
        /// </summary>
        #endregion
        private sealed class SQL12 : IQueryStorage
        {

            public string GetProfileInfo
            {
                get
                {
                    return @"select Id, Class from Profiles where 
                                ApplicationName = {0} and
                                ({1} is null or Usename = {1}) and
                                ({2} is null or LastActivityDate <= {2}) and
                                IsAnonymous <> {3} order by Username offset ({4} * {5}) rows fetch next {5} rows only";
                }
            }

            public string GetProfileInfoTotalRecords
            {
                get
                {
                    return @"select count(*) from Profiles where 
                                ApplicationName = {0} and
                                ({1} is null or Usename = {1}) and
                                ({2} is null or LastActivityDate <= {2}) and
                                IsAnonymous <> {3}";
                }
            }

        }


        #endregion

        #region Fields

        private const string NAME = "LightOneProfileProvider";
        private const string eventLog = "Application";
        private string eventSource = NAME;
        
        private string exceptionMessage = "An exception occurred. Please check the event log.";

        private IQueryStorage query;
        private Type profileType;
        private bool initialized;

        #endregion

        #region Properties

        public override string ApplicationName
        {
            get;
            set;
        }

        public bool pWriteExceptionsToEventLog
        {
            get;
            set;
        }


        #endregion

        #region Constructors

        public LightOneProfileProvider() : base()
        {}

        #endregion

        #region Methods

        public ProfileBase CreateProfile()
        {
            return (ProfileBase)Activator.CreateInstance(this.profileType);
        }

        /// <summary>
        /// 
        /// </summary>
        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {

            if (this.initialized) return;

            if (config == null)
                throw new ArgumentNullException("config");

            if (name == null || name.Length == 0)
                name = NAME;

            if (String.IsNullOrWhiteSpace(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "LightOne profile provider");
            }

            //chamada à classe base após as verificações necessárias.
            base.Initialize(name, config);

            //definindo o nome da aplicação.

            this.ApplicationName = GetConfigValue(config["applicationName"],
                                      System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);

            string typeName = GetConfigValue(config["queryStorageType"], typeof(SQL12).AssemblyQualifiedName);
            this.query = QueryStorageProvider.Instance(typeName);

            string profileTypeName = GetConfigValue(config["profileType"], typeof(ProfileBase).AssemblyQualifiedName);
            this.profileType = Type.GetType(profileTypeName);

            this.initialized = true;
        }

        /// <summary>
        /// 
        /// </summary>
        public override int DeleteInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
        {
            var profiles = ProfileBase.FindInactiveUsers(userInactiveSinceDate, authenticationOption);

            string[] userNames = profiles.Select(p => p.UserName).ToArray();

            return DeleteProfiles(userNames);
        }

        /// <summary>
        /// 
        /// </summary>
        public override int DeleteProfiles(string[] usernames)
        {
            int deleteCount = 0;
            var dbe = DBEngine.GetInstance();
            using (IDbTransaction trx = dbe.BeginTransaction())
            {
                try
                {
                    foreach (string userName in usernames)
                    {
                        if (DeleteProfile(userName, trx))
                            deleteCount++;
                    }
                    trx.Commit();
                }
                catch (Exception except)
                {
                    trx.Rollback();
                    deleteCount = 0;
                }
            }
            return deleteCount;
        }

        /// <summary>
        /// 
        /// </summary>
        public override int DeleteProfiles(ProfileInfoCollection profiles)
        {
            int deleteCount = 0;

            var dbe = DBEngine.GetInstance();
            using (IDbTransaction trx = dbe.BeginTransaction())
            {
                try
                {
                    foreach (ProfileInfo p in profiles)
                    {
                        if (DeleteProfile(p.UserName, trx))
                            deleteCount++;
                    }
                }
                catch (Exception except)
                {
                    trx.Rollback();
                    deleteCount = 0;
                }
            }

            return deleteCount;
        }

        /// <summary>
        /// 
        /// </summary>
        public override ProfileInfoCollection FindInactiveProfilesByUserName(ProfileAuthenticationOption authenticationOption, string usernameToMatch, DateTime userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, usernameToMatch, userInactiveSinceDate,
                  pageIndex, pageSize, out totalRecords);
        }

        /// <summary>
        /// 
        /// </summary>
        public override ProfileInfoCollection FindProfilesByUserName(ProfileAuthenticationOption authenticationOption, string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, usernameToMatch,
                null, pageIndex, pageSize, out totalRecords);
        }

        /// <summary>
        /// 
        /// </summary>
        public override ProfileInfoCollection GetAllInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, null, userInactiveSinceDate,
                  pageIndex, pageSize, out totalRecords);
        }

        /// <summary>
        /// 
        /// </summary>
        public override ProfileInfoCollection GetAllProfiles(ProfileAuthenticationOption authenticationOption, int pageIndex, int pageSize, out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, null, null,
                  pageIndex, pageSize, out totalRecords);
        }

        /// <summary>
        /// 
        /// </summary>
        public override int GetNumberOfInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
        {
            int inactiveProfiles = 0;

            ProfileInfoCollection profiles =
              GetProfileInfo(authenticationOption, null, userInactiveSinceDate,
                  0, 0, out inactiveProfiles);

            return inactiveProfiles;
        }

        /// <summary>
        /// 
        /// </summary>
        public override System.Configuration.SettingsPropertyValueCollection GetPropertyValues(System.Configuration.SettingsContext context, System.Configuration.SettingsPropertyCollection collection)
        {
            string username = (string)context["UserName"];
            bool isAuthenticated = (bool)context["IsAuthenticated"];
            var profile = ProfileBase.FindByUserName(username, this.ApplicationName, isAuthenticated);
            // The serializeAs attribute is ignored in this provider implementation.

            if (profile == null)
            {
                throw new ProviderException("User profile not exists.");
            }

            SettingsPropertyValueCollection svc =
                new SettingsPropertyValueCollection();

            foreach (SettingsProperty prop in collection)
            {
                try
                {
                    SettingsPropertyValue pv = new SettingsPropertyValue(prop);
                    var property = this.profileType.GetProperty(prop.Name);
                    pv.PropertyValue = property.GetValue(profile, null);
                    svc.Add(pv);
                }
                catch (Exception except)
                {
                    throw new ProviderException("Unsupported property.", except);
                }
            }

            UpdateActivityDates(username, isAuthenticated, true);

            return svc;
        }

        /// <summary>
        /// 
        /// </summary>
        public override void SetPropertyValues(System.Configuration.SettingsContext context, System.Configuration.SettingsPropertyValueCollection collection)
        {
            // The serializeAs attribute is ignored in this provider implementation.
            
            string username = (string)context["UserName"];
            bool isAuthenticated = (bool)context["IsAuthenticated"];
            var profile = ProfileBase.FindByUserName(username, this.ApplicationName, isAuthenticated);

            if (profile == null)
            {
                profile = this.CreateProfile();
                profile.UserName = username;
                profile.Application = this.ApplicationName;
                profile.IsAnonymous = !isAuthenticated;
            }

            foreach (SettingsPropertyValue pv in collection)
            {
                try
                {
                    var property = this.profileType.GetProperty(pv.Name);
                    property.SetValue(profile, pv.PropertyValue, null);
                }
                catch (Exception except)
                {
                    throw new ProviderException("Unsupported property.", except);
                }
            }

            UpdateActivityDates(username, isAuthenticated, false);
        }

        #region Private Methods

        /// <summary>
        /// 
        /// </summary>
        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrWhiteSpace(configValue))
                return defaultValue;

            return configValue;
        }

        /// <summary>
        /// 
        /// </summary>
        private void CheckParameters(int pageIndex, int pageSize)
        {
            if (pageIndex < 0)
                throw new ArgumentException("Page index must 0 or greater.");
            if (pageSize < 1)
                throw new ArgumentException("Page size must be greater than 0.");
        }

        /// <summary>
        /// 
        /// </summary>
        private void UpdateActivityDates(string userName, bool isAuthenticated, bool activityOnly)
        {
            DateTime activityDate = DateTime.Now;

            var userProfile = ProfileBase.FindByUserName(userName, this.ApplicationName, isAuthenticated);

            if (userProfile != null)
            {
                if (activityOnly)
                {
                    userProfile.LastActivityDate = activityDate;
                }
                userProfile.Save();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private ProfileInfoCollection GetProfileInfo(
          ProfileAuthenticationOption authenticationOption,
          string usernameToMatch,
          DateTime? userInactiveSinceDate,
          int pageIndex,
          int pageSize,
          out int totalRecords)
        {
            DBEngine dbe = DBEngine.GetInstance();
            using (IDbTransaction trx = dbe.BeginTransaction())
            {
                try
                {
                    totalRecords = Convert.ToInt32(dbe.ExecuteScalar(this.query.GetProfileInfoTotalRecords, this.ApplicationName, usernameToMatch, userInactiveSinceDate.HasValue ? (object)userInactiveSinceDate.Value : null, authenticationOption == ProfileAuthenticationOption.Authenticated ? true : false));
                    var result = ProfileBase.FindBySql(this.query.GetProfileInfo, this.ApplicationName, usernameToMatch, userInactiveSinceDate.HasValue ? (object)userInactiveSinceDate.Value : null, authenticationOption == ProfileAuthenticationOption.Authenticated ? true : false, pageIndex, pageSize);
                    trx.Commit();

                    ProfileInfoCollection profiles = new ProfileInfoCollection();
                    foreach (var profile in result)
                    {
                        profiles.Add(
                            new ProfileInfo(profile.UserName, profile.IsAnonymous, profile.LastActivityDate, profile.LastPersistence, 0)
                            );
                    }

                    return profiles;
                }
                catch (Exception except)
                {
                    trx.Rollback();
                    throw except;
                }
            }
        }


        /// <summary>
        /// 
        /// </summary>
        private bool DeleteProfile(string username, IDbTransaction trx)
        {
            var userProfile = ProfileBase.FindByUserNameIgnoreAuthentication(username, this.ApplicationName);
            if (userProfile != null)
            {
                userProfile.Delete(trx); // se o delete falhar ele solta exception
            }

            return true;
        }

        #endregion
        #endregion
    }
}
